Um mergulho profundo no hook useActionState do React. Aprenda a gerenciar estados de formulário, lidar com UI pendente e simplificar ações assíncronas.
Dominando o useActionState do React: O Guia Definitivo para Gerenciamento Moderno de Formulários e Ações
No cenário em constante evolução do desenvolvimento web, o React continua a introduzir ferramentas poderosas que refinam como construímos interfaces de usuário. Uma das adições recentes mais significativas, solidificando seu lugar no React 19, é o hook `useActionState`. Anteriormente conhecido como `useFormState` em releases experimentais, este hook é muito mais do que uma utilidade de formulário; é uma mudança fundamental na forma como gerenciamos o estado relacionado a operações assíncronas.
Este guia abrangente levará você dos conceitos fundamentais a padrões avançados, demonstrando por que `useActionState` é um divisor de águas para lidar com mutações de dados, comunicação com o servidor e feedback do usuário em aplicações React modernas. Seja construindo um formulário de contato simples ou um painel complexo e intensivo em dados, dominar este hook simplificará dramaticamente seu código e melhorará a experiência do usuário.
O Problema Central: A Complexidade do Gerenciamento Tradicional de Estado de Ação
Antes de mergulharmos na solução, vamos apreciar o problema. Por anos, lidar com o estado em torno de uma simples submissão de formulário ou uma chamada de API envolvia um padrão previsível, mas incômodo, usando `useState` e `useEffect`. Desenvolvedores em todo o mundo escreveram esse código boilerplate inúmeras vezes.
Considere um formulário de login padrão. Precisamos gerenciar:
- Os valores de entrada do formulário (email, senha).
- Um estado de carregamento ou pendente para desativar o botão de envio e fornecer feedback.
- Um estado de erro para exibir mensagens do servidor (por exemplo, "Credenciais inválidas").
- Um estado de sucesso ou dados de uma submissão bem-sucedida.
O Exemplo 'Antes': Usando `useState`
Uma implementação típica pode parecer com isto:
// Uma abordagem tradicional sem useActionState
import { useState } from 'react';
// Uma função mock de API
async function loginUser(email, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (email === 'user@example.com' && password === 'password123') {
resolve({ success: true, message: 'Bem-vindo de volta!' });
} else {
reject(new Error('Email ou senha inválidos.'));
}
}, 1500);
});
}
function TraditionalLoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
setIsLoading(true);
setError(null);
try {
const result = await loginUser(email, password);
// Lidar com login bem-sucedido, por exemplo, redirecionar ou exibir mensagem de sucesso
alert(result.message);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
);
}
Este código funciona, mas tem várias desvantagens:
- Boilerplate: Precisamos de três chamadas separadas de `useState` (`error`, `isLoading` e para cada input) para gerenciar o ciclo de vida da ação.
- Gerenciamento Manual de Estado: Somos responsáveis por definir manualmente `isLoading` como true, depois false em um bloco `finally`, e limpar erros anteriores no início de uma nova submissão. Isso é propenso a erros.
- Acoplamento: A lógica de submissão está firmemente acoplada ao manipulador de eventos do componente.
Introduzindo `useActionState`: Uma Mudança de Paradigma na Simplicidade
`useActionState` é um Hook do React projetado para gerenciar o estado de uma ação. Ele gerencia elegantemente o ciclo de pendência, conclusão e erro, reduzindo o boilerplate e promovendo um código mais limpo e declarativo.
Entendendo a Assinatura do Hook
A sintaxe do hook é simples e poderosa:
const [state, formAction] = useActionState(action, initialState);
- `action`: Uma função assíncrona que executa a operação desejada (por exemplo, chamada de API, ação do servidor). Ela recebe o estado anterior e quaisquer argumentos específicos da ação (como dados do formulário) e deve retornar o novo estado.
- `initialState`: O valor do `state` antes da ação ter sido executada.
- `state`: O estado atual. Ele mantém o `initialState` inicialmente e, após a execução da ação, ele detém o valor retornado pela ação. É aqui que você armazenará mensagens de sucesso, detalhes de erro ou feedback de validação.
- `formAction`: Uma nova versão encapsulada de sua função `action`. Você passa esta função para a prop `action` do seu `
O Exemplo 'Depois': Refatorando com `useActionState`
Vamos refatorar nosso formulário de login. Observe o quão mais limpo e focado o componente se torna.
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
// A função de ação é agora definida fora do componente.
// Ela recebe o estado anterior e os dados do formulário.
async function loginAction(previousState, formData) {
const email = formData.get('email');
const password = formData.get('password');
// Simular atraso de rede
await new Promise(resolve => setTimeout(resolve, 1500));
if (email === 'user@example.com' && password === 'password123') {
return { success: true, message: 'Login bem-sucedido! Bem-vindo.' };
} else {
return { success: false, message: 'Email ou senha inválidos.' };
}
}
// Um componente separado para mostrar o estado pendente.
// Este é um padrão chave para separação de responsabilidades.
function SubmitButton() {
const { pending } = useFormStatus();
return (
);
}
function ActionStateLoginForm() {
const initialState = { success: false, message: null };
const [state, formAction] = useActionState(loginAction, initialState);
return (
);
}
As melhorias são imediatamente óbvias:
- Gerenciamento de Estado Manual Zero: Não gerenciamos mais `isLoading` ou estados de `error` nós mesmos. O React gerencia isso internamente.
- Lógica Desacoplada: A função `loginAction` é agora uma função pura e reutilizável que pode ser testada isoladamente.
- UI Declarativa: O JSX do componente renderiza declarativamente a UI com base no `state` retornado pelo hook. Se `state.message` existir, nós o exibimos.
- Estado Pendente Simplificado: Introduzimos `useFormStatus`, um hook complementar que torna o gerenciamento do estado pendente trivial.
Principais Recursos e Benefícios do `useActionState`
1. Gerenciamento Contínuo do Estado Pendente com `useFormStatus`
Um dos recursos mais poderosos deste padrão é sua integração com o hook `useFormStatus`. `useFormStatus` fornece informações sobre o status da submissão do `
async function deleteItemAction(prevState, itemId) {
// Simular uma chamada de API para excluir um item
console.log(`Excluindo item com ID: ${itemId}`);
await new Promise(res => setTimeout(res, 1000));
const isSuccess = Math.random() > 0.2; // Simular falha potencial
if (isSuccess) {
return { success: true, message: `Item ${itemId} excluído.` };
} else {
return { success: false, message: 'Falha ao excluir item. Por favor, tente novamente.' };
}
}
function DeletableItem({ id }) {
const [state, deleteAction] = useActionState(deleteItemAction, { message: null });
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(() => {
deleteAction(id);
});
};
return (
Item {id}
{state.message && {state.message}
}
);
}
Observação: Quando `useActionState` não é usado dentro de um `
Atualizações Otimistas com `useOptimistic`
Para uma experiência de usuário ainda melhor, `useActionState` pode ser combinado com o hook `useOptimistic`. Atualizações otimistas envolvem atualizar a UI imediatamente, *assumindo* que uma ação será bem-sucedida, e depois reverter a alteração apenas se ela falhar. Isso faz com que a aplicação pareça instantânea.
Considere uma lista simples de mensagens. Quando uma nova mensagem é enviada, queremos que ela apareça na lista imediatamente.
import { useActionState, useOptimistic, useRef } from 'react';
async function sendMessageAction(prevState, formData) {
const sentMessage = formData.get('message');
await new Promise(res => setTimeout(res, 2000)); // Simular rede lenta
// Em um app real, esta seria sua chamada de API
// Para esta demonstração, assumiremos que sempre terá sucesso
return { text: sentMessage, sending: false };
}
function MessageList() {
const formRef = useRef();
const [messages, setMessages] = useState([{ text: 'Olá!', sending: false }]);
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(currentMessages, newMessageText) => [
...currentMessages,
{ text: newMessageText, sending: true }
]
);
const formAction = async (formData) => {
const newMessageText = formData.get('message');
addOptimisticMessage(newMessageText);
formRef.current.reset(); // Limpar o formulário visualmente
const result = await sendMessageAction(null, formData);
// Atualizar o estado final
setMessages(current => [...current, result]);
};
return (
Chat
{optimisticMessages.map((msg, index) => (
-
{msg.text} {msg.sending && (Enviando...)}
))}
);
}
Neste exemplo mais complexo, vemos como `useOptimistic` adiciona imediatamente a mensagem com um rótulo "(Enviando...)". A `formAction` então executa a operação assíncrona real. Assim que ela é concluída, o estado final é atualizado. Se a ação falhasse, o React descartaria automaticamente o estado otimista e reverteria para o estado original `messages`.
`useActionState` vs. `useState`: Quando Escolher Qual
Com esta nova ferramenta, surge uma pergunta comum: quando devo ainda usar `useState`?
-
Use `useState` para:
- Estado de UI puramente do lado do cliente e síncrono: Pense em alternar um modal, gerenciar a aba atual em um grupo de abas ou lidar com inputs de componentes controlados que não acionam diretamente uma ação do servidor.
- Estado que não é o resultado direto de uma ação: Por exemplo, armazenar configurações de filtro que são aplicadas no lado do cliente.
- Variáveis de estado simples: Um contador, um sinalizador booleano, uma string.
-
Use `useActionState` para:
- Estado que é atualizado como resultado de uma submissão de formulário ou uma ação assíncrona: Este é o seu principal caso de uso.
- Quando você precisa rastrear os estados pendente, de sucesso e de erro de uma operação: Ele encapsula todo esse ciclo de vida perfeitamente.
- Integração com React Server Actions: É o hook essencial do lado do cliente para trabalhar com Server Actions.
- Formulários que exigem validação e feedback do lado do servidor: Ele fornece um canal limpo para o servidor retornar erros de validação estruturados para o cliente.
Melhores Práticas Globais e Considerações
Ao construir para um público global, é crucial considerar fatores além da funcionalidade do código.
Acessibilidade (a11y)
Ao exibir erros de formulário, certifique-se de que eles sejam acessíveis a usuários de tecnologias assistivas. Use atributos ARIA para anunciar mudanças dinamicamente.
// Em seu componente de formulário
const { errors } = state;
// ...
{errors?.email && (
{errors.email}
)}
O atributo `aria-invalid="true"` sinaliza para leitores de tela que o input tem um erro. O `role="alert"` na mensagem de erro garante que ela seja anunciada ao usuário assim que aparecer.
Internacionalização (i18n)
Evite retornar strings de erro codificadas de suas ações, especialmente em uma aplicação multilíngue. Em vez disso, retorne códigos ou chaves de erro que possam ser mapeados para strings traduzidas no cliente.
// Ação no servidor
async function internationalizedAction(prevState, formData) {
// ...lógica de validação...
if (password.length < 8) {
return { success: false, error: { code: 'ERROR_PASSWORD_TOO_SHORT' } };
}
// ...
}
// Componente no cliente
import { useTranslation } from 'react-i18next';
function I18nForm() {
const { t } = useTranslation();
const [state, formAction] = useActionState(internationalizedAction, {});
return (
{/* ... inputs ... */}
{state.error && (
{t(state.error.code)} // Mapeia 'ERROR_PASSWORD_TOO_SHORT' para 'A senha deve ter pelo menos 8 caracteres.'
)}
);
}
Segurança de Tipos com TypeScript
Usar TypeScript com `useActionState` fornece excelente segurança de tipos, capturando bugs antes que aconteçam. Você pode definir tipos para o estado e a carga útil da sua ação.
import { useActionState } from 'react';
// 1. Defina a forma do estado
type FormState = {
success: boolean;
message: string | null;
errors?: {
email?: string;
password?: string;
} | null;
};
// 2. Defina a assinatura da função de ação
type SignupAction = (prevState: FormState, formData: FormData) => Promise;
const signupAction: SignupAction = async (prevState, formData) => {
// ... implementação ...
// TypeScript garantirá que você retorne um objeto FormState válido.
return { success: false, message: 'Inválido.', errors: { email: '...' } };
};
function TypedSignupForm() {
const initialState: FormState = { success: false, message: null, errors: null };
// 3. O hook infere os tipos corretamente
const [state, formAction] = useActionState(signupAction, initialState);
// Agora, `state` é totalmente tipado. `state.errors.email` será verificado pelo tipo.
return (
{/* ... */}
);
}
Conclusão: O Futuro do Gerenciamento de Estado no React
O hook `useActionState` é mais do que apenas uma conveniência; representa uma peça central da filosofia evolutiva do React. Ele incentiva os desenvolvedores a uma separação de preocupações mais clara, aplicações mais resilientes através da melhoria progressiva e uma maneira mais declarativa de lidar com os resultados das ações do usuário.
Ao centralizar a lógica de uma ação e seu estado resultante, `useActionState` elimina uma fonte significativa de boilerplate e complexidade do lado do cliente. Ele se integra perfeitamente com `useFormStatus` para estados pendentes e `useOptimistic` para experiências de usuário aprimoradas, formando um trio poderoso para padrões modernos de mutação de dados.
Ao construir novos recursos ou refatorar os existentes, considere usar `useActionState` sempre que estiver gerenciando o estado que resulta diretamente de uma operação assíncrona. Isso levará a um código mais limpo, mais robusto e perfeitamente alinhado com a direção futura do React.